/// <reference path="../typings/tsd.d.ts" />

import fs = require("fs");
import async = require("async");
import marked = require("marked");
import FileTreeWalker = require("./fileTreeWalker");
import HTMLContentMaker = require("./htmlContentMaker");

interface MarkBuilderConfig {

    "name": string;

    "urlRoot": string;

    "smartTemplate"?: boolean;

    "smartStyle"?: boolean;

    "outDir"?: string;

    "contentText"?: string;
}

class MarkBuilder {

    protected prjRoot: string;

    protected distRoot: string;

    protected cfgRoot: string;

    protected customConfigRoot: boolean;

    public config: MarkBuilderConfig;

    public templates: { [key: string]: string };

    public styles: string[];

    constructor(prjRoot?: string, distRoot?: string, cfgRoot?: string) {

        prjRoot && (this.projectRoot = prjRoot);
        distRoot && (this.targetRoot = distRoot);
        cfgRoot && (this.customConfigRoot = true) && (this.cfgRoot = cfgRoot);
    }

    public isValid(): boolean {

        return this.prjRoot === null;
    }

    public get projectRoot(): string {

        return this.prjRoot;
    }

    public set projectRoot(newVal: string) {

        if (newVal.substr(-1) !== "/") {

            newVal += "/";
        }

        this.prjRoot = newVal;

        if (!this.customConfigRoot) {

            this.cfgRoot = this.prjRoot + ".config/";
        }
    }

    public get targetRoot(): string {

        return this.distRoot;
    }

    public set targetRoot(newVal: string) {

        if (newVal.substr(-1) !== "/") {

            newVal += "/";
        }

        this.distRoot = newVal;
    }

    public get configRoot(): string {

        return this.cfgRoot;
    }

    public set configRoot(newVal: string) {

        if (newVal.substr(-1) !== "/") {

            newVal += "/";
        }

        this.cfgRoot = newVal;
        this.customConfigRoot = true;
    }

    protected loadConfig(next: ErrorCallback): void {

        let _this: MarkBuilder = this;

        fs.readFile(this.cfgRoot + "project.json", "utf-8", function(err: Error, data: string): void {

            if (err) {

                next(err);

                return;
            }

            try {

                _this.config = JSON.parse(data);

            } catch (e) {

                next({
                    "name": "BAD-CONFIG",
                    "message": "Unrecognizable config file."
                });

                return;
            }

            if (_this.config.outDir && !_this.targetRoot) {

                _this.targetRoot = _this.projectRoot + _this.config.outDir;
            }

            next();
        });
    }

    protected applyTemplate(template: string, elements: { [key: string]: string }): string {

        let result: string = template;

        for (let key in elements) {

            result = result.replace(new RegExp("\{" + key + "\}"), elements[key]);
        }

        return result;
    }

    protected parseFile(mdSource: string, html: string, next: AsyncResultCallback<string>): void {

        let builder: MarkBuilder = this;
        let asyncMode = false;
        let templateName: string = "default.htm";
        let styleName: string = "default.css";
        let series: AsyncFunction<{}>[] = [];
        let chapterTitle: string;
        let matches: RegExpMatchArray;

        if (matches = mdSource.match(/\[markdown-title\]\:\s*(\S+)/)) {

            chapterTitle = matches[1];

        } else if (matches = html.match(/<h1(\s+id\="[^"]+")?>(.+)<\/h1>/i)) {

            chapterTitle = matches[2];

        }

        if (this.config.smartTemplate) {

            if (matches = mdSource.match(/\[markdown-template\]\:\s*(\S+)/)) {

                templateName = matches[1];

                if (!this.templates[templateName]) {

                    asyncMode = true;

                    series.push(function(next: ErrorCallback): void {

                        fs.readFile(builder.configRoot + "templates/" + templateName, "utf-8", function (err: Error, text: string) {

                            if (err) {

                                next(err);

                                return;
                            }

                            builder.templates[templateName] = text;

                            next();
                        });

                    });
                }
            }

        }

        if (this.config.smartStyle) {

            if (matches = mdSource.match(/\[markdown-style\]\:\s*(\S+)/)) {

                styleName = matches[1];

                if (this.styles.indexOf(styleName) === -1) {

                    asyncMode = true;

                    series.push(function(next: ErrorCallback): void {

                        fs.exists(builder.configRoot + "styles/" + styleName, function(exists: boolean) {

                            if (!exists) {

                                next({
                                    "name": "BAD-STYLE",
                                    "message": "Specific styles file '" + styleName + "' not found."
                                });

                                return;
                            }

                            builder.styles.push(styleName);

                            next();
                        });

                    });
                }
            }

        }

        if (mdSource.indexOf("[markdown-menu]") >= 0) {

            let contentMaker: HTMLContentMaker = new HTMLContentMaker();

            contentMaker.contentText = this.config.contentText ? this.config.contentText : "Content";

            if (!contentMaker.extract(html)) {

                next({
                    "name": "BAD-MENU",
                    "message": "Menu structure is wrong."
                }, null);

                return;

            }

            html = html.replace("<p>[markdown-menu]</p>", contentMaker.generate());
        }

        if (asyncMode) {

            async.series(series, function(err?: Error) {

                if (err) {
                    next(err, null);

                    return;
                }

                next(null, builder.applyTemplate(builder.templates[templateName], {
                    "title": chapterTitle + " - " + builder.config.name,
                    "text": html,
                    "style-ref": `<link rel="stylesheet" type="text/css" href="${builder.config.urlRoot}styles/${styleName}" />`
                }));

            });

        } else {

            next(null, this.applyTemplate(this.templates[templateName], {
                "title": chapterTitle + " - " + this.config.name,
                "text": html,
                "style-ref": `<link rel="stylesheet" type="text/css" href="${builder.config.urlRoot}styles/${styleName}" />`
            }));

        }

    }

    /** Handle the *.md files. */
    protected handleFile(outputPath: string, sourcePath: string, next: ErrorCallback): void {

        let builder: MarkBuilder = this;

        async.waterfall([

            /** read a *.md file. */
            function(next: AsyncResultCallback<string>) {
                fs.readFile(sourcePath, "utf-8", next);
            },

            /** Parse markdown text. */
            function(mdSource: string, next: AsyncResultCallback<any>): void {

                mdSource = mdSource.replace(/\.md(\s|\)|\#|")?/g, ".html$1");

                marked(mdSource, function(err: Error, htmlContent: string): void {

                    next(err, {
                        "mdSource": mdSource,
                        "html": htmlContent
                    });

                });
            },

            /** Build the output text */
            function (content: {
                "mdSource": string;
                "html": string
            }, next: AsyncResultCallback<string>): void {

                builder.parseFile(content.mdSource, content.html, next);
            },

            function (htmlResult: string, next: AsyncResultCallback<string>): void {

                fs.writeFile(outputPath, htmlResult, next);

            }

        ], next);

    }

    /** auto create non-existent directories. */
    protected handleDirectory(outputPath: string, next: ErrorCallback): void {

        fs.exists(outputPath, function(exists: boolean) {

            if (!exists) {

                fs.mkdir(outputPath, next);

                return;
            }

            next();
        });

    }

    public build(complete: ErrorCallback): void {

        let builder: MarkBuilder = this;

        this.templates = {};
        this.styles = [];

        async.series([

            /** Load configuration. */
            function(next: ErrorCallback) {

                builder.loadConfig(next);

            },

            /** Load default template */
            function(next: ErrorCallback) {

                fs.exists(builder.targetRoot, function(exists: boolean) {

                    if (exists) {

                        next();

                        return;
                    }

                    fs.mkdir(builder.targetRoot, next);
                });

            },

            /** Load default template */
            function(next: ErrorCallback) {

                fs.readFile(builder.configRoot + "templates/default.htm", "utf-8", function(err: Error, text: string) {

                    if (err) {

                        next(err);

                        return;
                    }

                    builder.templates["default.htm"] = text;

                    next();
                });

            },

            /** Detect default styles */
            function(next: ErrorCallback) {

                fs.exists(builder.configRoot + "styles/default.css", function(exists: boolean) {

                    if (!exists) {

                        next({
                            "name": "BAD-STYLE",
                            "message": "Specific styles file 'default.css' not found."
                        });

                        return;
                    }

                    builder.styles.push("default.css");

                    next();
                });

            },

            /** enum *.md files and handle them */
            function(next: ErrorCallback) {

                FileTreeWalker(builder.projectRoot, function(filePath: string, isDir: boolean, next: ErrorCallback) {

                    if (isDir) {

                        builder.handleDirectory(builder.targetRoot + filePath.substr(builder.projectRoot.length), next);

                    } else {

                        builder.handleFile(builder.targetRoot + filePath.substr(builder.projectRoot.length, filePath.length - builder.projectRoot.length - 2) + "html", filePath, next);

                    }

                }, next);

            },

            /** Make the styles directory */
            function(next: ErrorCallback) {

                fs.exists(builder.targetRoot + "styles", function(exists: boolean) {

                    if (exists) {

                        next();

                        return;
                    }

                    fs.mkdir(builder.targetRoot + "styles", next);
                });

            },

            /** Copy the style files */
            function(next: ErrorCallback) {

                let targetPath = builder.targetRoot + "styles/";
                let sourcePath = builder.configRoot + "styles/";

                async.forEachOf(builder.styles, function(fileName: string, index: number, next: ErrorCallback) {

                    fs.readFile(sourcePath + fileName, function(err: Error, content: Buffer) {

                        if (err) {

                            next(err);

                            return;
                        }

                        fs.writeFile(targetPath + fileName, content, next);

                    });

                }, next);

            }

        ], complete);

    }

}

export = MarkBuilder;
